//
// JarComparator
//

package net.jextra.crack.tool;

import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.util.zip.*;

public class JarComparator
{
    // ============================================================
    // Fields
    // ============================================================

    private boolean ignoreTimestamps;
    private boolean ignoreCVSFiles;
    private boolean compareCRCValues;
    private Pattern filesToIgnorePattern;
    private boolean verbose;

    private ZipFile zipFile1;
    private ZipFile zipFile2;

    // ============================================================
    // Constructors
    // ============================================================

    public JarComparator()
    {
        ignoreTimestamps = true;
        ignoreCVSFiles = false;
        compareCRCValues = true;
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    public void setIgnoreTimestamps( boolean b )
    {
        ignoreTimestamps = b;
    }

    public boolean isIgnoringTimestamps()
    {
        return ignoreTimestamps;
    }

    public boolean ignoreCVSFiles()
    {
        return ignoreCVSFiles;
    }

    public void setIgnoreCVSFiles( boolean b )
    {
        ignoreCVSFiles = b;
    }

    public void setCompareCRCValues( boolean b )
    {
        compareCRCValues = b;
    }

    public boolean getCompareCRCValues()
    {
        return compareCRCValues;
    }

    public void setVerbose( boolean b )
    {
        verbose = b;
    }

    public void setFilenameRegexToIgnore( Set<String> patterns )
    {
        if ( patterns == null || patterns.isEmpty() )
        {
            filesToIgnorePattern = null;
        }
        else
        {
            String regex = "";
            for ( Iterator<String> iter = patterns.iterator(); iter.hasNext(); )
            {
                String pattern = iter.next();
                if ( regex.length() > 0 )
                {
                    regex = regex + "|";
                }
                regex = regex + "(" + pattern + ")";
            }

            filesToIgnorePattern = Pattern.compile( regex );
            if ( verbose )
            {
                System.out.println( "Regular expression is : " + regex );
            }
        }
    }

    public static boolean isZipFile( String filename )
    {
        boolean result;
        if ( filename == null )
        {
            result = false;
        }
        else
        {
            String lowercaseName = filename.toLowerCase();
            if ( lowercaseName.endsWith( ".zip" ) )
            {
                result = true;
            }
            else if ( lowercaseName.endsWith( ".ear" ) )
            {
                result = true;
            }
            else if ( lowercaseName.endsWith( ".war" ) )
            {
                result = true;
            }
            else if ( lowercaseName.endsWith( ".rar" ) )
            {
                result = true;
            }
            else if ( lowercaseName.endsWith( ".jar" ) )
            {
                result = true;
            }
            else
            {
                result = false;
            }
        }
        return result;
    }

    public List<String> calculateDifferences( ZipFile zf1, ZipFile zf2 )
        throws IOException
    {
        zipFile1 = zf1;
        zipFile2 = zf2;

        Map<String, ZipEntry> map1 = buildZipEntryMap( zipFile1 );
        Map<String, ZipEntry> map2 = buildZipEntryMap( zipFile2 );

        List<String> diffs = calculateDifferences( map1, map2 );

        zipFile1.close();
        zipFile2.close();

        return diffs;
    }

    // ----------
    // protected
    // ----------

    protected boolean ignoreThisFile( String filepath, String filename )
    {
        if ( filename == null )
        {
            return false;
        }
        if ( isCVSFile( filepath, filename ) && ignoreCVSFiles() )
        {
            return true;
        }
        if ( filesToIgnorePattern == null )
        {
            return false;
        }
        Matcher m = filesToIgnorePattern.matcher( filename );
        boolean match = m.matches();
        if ( match && verbose )
        {
            System.out.println( "Found a match against : " + filename + " so excluding" );
        }
        return match;
    }

    protected boolean isCVSFile( String filepath, String filename )
    {
        if ( filename == null )
        {
            return false;
        }
        return filepath.indexOf( "CVS" ) != -1 || filename.equals( "CVS" );
    }

    protected Map<String, ZipEntry> buildZipEntryMap( ZipFile zf )
        throws java.io.IOException
    {
        Map<String, ZipEntry> zipEntryMap = new HashMap<String, ZipEntry>();
        try
        {
            Enumeration<? extends ZipEntry> enumeration = zf.entries();
            for ( ZipEntry entry = enumeration.nextElement(); enumeration.hasMoreElements(); entry = enumeration
                .nextElement() )
            {
                processZipEntry( "", entry, zf, zipEntryMap );
            }
        }
        finally
        {
            // zf.close();
        }

        return zipEntryMap;
    }

    protected void processZipEntry( String prefix, ZipEntry zipEntry, ZipFile zf, Map<String, ZipEntry> zipEntryMap )
        throws IOException
    {
        if ( ignoreThisFile( prefix, zipEntry.getName() ) )
        {
            if ( verbose )
            {
                System.out.println( "ignoring file: " + zipEntry.getName() );
            }
        }
        else
        {
            String name = prefix + zipEntry.getName();

            if ( verbose )
            {
                System.out.println( "processing ZipEntry: " + name );
            }

            if ( zipEntry.isDirectory() )
            {
                zipEntryMap.put( name, zipEntry );
            }
            else if ( isZipFile( name ) )
            {
                // TODO: punt! need the input stream of the specific zip entry, cant get it because
                // what is passed in is not the stream os the entry, but of the overall zipfile.
                // processEmbeddedZipFile( zipEntry.getName() + "/", zis, zipEntryMap );
                zipEntryMap.put( name, zipEntry );
            }
            else
            {
                zipEntryMap.put( name, zipEntry );
            }
        }
    }

    // TODO: see above punt on this one.
    // protected void processEmbeddedZipFile( String prefix, ZipInputStream zis,
    // Map<String,ZipEntry> m )
    // throws IOException
    // {
    // for ( ZipEntry entry = zis.getNextEntry(); entry != null; entry = zis.getNextEntry() )
    // {
    // processZipEntry( prefix, entry, zis, m );
    // zis.closeEntry();
    // }
    // }

    protected List<String> calculateDifferences( Map<String, ZipEntry> m1, Map<String, ZipEntry> m2 )
        throws IOException
    {
        List<String> returnval = new ArrayList<String>();

        Set<String> names1 = m1.keySet();
        Set<String> names2 = m2.keySet();
        Set<String> allNames = new HashSet<String>();
        allNames.addAll( names1 );
        allNames.addAll( names2 );
        for ( Iterator<String> iterAllNames = allNames.iterator(); iterAllNames.hasNext(); )
        {
            String name = iterAllNames.next();
            if ( names1.contains( name ) && !names2.contains( name ) )
            {
                returnval.add( "file removed: " + ( (ZipEntry) m1.get( name ) ).getName() );
            }
            else if ( names2.contains( name ) && !names1.contains( name ) )
            {
                returnval.add( "file added: " + ( (ZipEntry) m2.get( name ) ).getName() );
            }
            else if ( names1.contains( name ) && names2.contains( name ) )
            {
                ZipEntry entry1 = (ZipEntry) m1.get( name );
                ZipEntry entry2 = (ZipEntry) m2.get( name );

                System.out.println( "  Comparing " + name + "." );

                if ( !entriesMatch( entry1, entry2 ) )
                {
                    returnval.add( "file changed: " + ( (ZipEntry) m1.get( name ) ).getName() + ", "
                        + ( (ZipEntry) m2.get( name ) ).getName() );
                }
            }
            else
            {
                throw new IllegalStateException( "unexpected state" );
            }
        }

        return returnval;
    }

    protected boolean entriesMatch( ZipEntry entry1, ZipEntry entry2 )
        throws IOException
    {
        if ( entry1.isDirectory() && entry2.isDirectory() )
        {
            return true;
        }

        if ( entry1.getSize() != entry2.getSize() )
        {
            return false;
        }

        //
        // If they are the same size, compare the contents.
        //
        InputStream is1 = zipFile1.getInputStream( entry1 );
        InputStream is2 = zipFile2.getInputStream( entry2 );

        byte[] buf1 = new byte[1024];
        byte[] buf2 = new byte[1024];

        boolean match = true;
        while ( true )
        {
            int r1 = is1.read( buf1 );
            int r2 = is2.read( buf2 );

            if ( r1 != r2 )
            {
                match = false;
                break;
            }

            if ( r1 < 1024 )
            {
                break;
            }

            for ( int i = 0; i < r1; ++i )
            {
                if ( buf1[i] != buf2[i] )
                {
                    match = false;
                    break;
                }
            }
        }

        return match;
    }
}
